package com.cloud.hotline.common.utils;

import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.sql.DataSource;

import org.apache.commons.lang.StringUtils;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;

import redis.clients.jedis.Jedis;

/**
 * 工具类，基于Redis生成数据库主键序列
 * 
 * @author taominglong
 *
 */
public class RedisKeyHelper {
	private static final String REDIS_TEMPLATE_BEAN_NAME = "redisTemplate";
	private static final int RETRY_LOCK_TIMES = 3;
	private static final String LOCK_SUCCESS = "OK";
	private static final String SET_IF_NOT_EXIST = "NX";
	private static final String SET_WITH_EXPIRE_TIME = "PX";
	private static final Long RELEASE_SUCCESS = 1L;
	private static final String LOCK_SUFFIX = "_LOCK";
	private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

	/**
	 * 基于Redis生成数据库主键
	 * 
	 * @param clazz
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static long genereateKey(Class<?> clazz) {
		if (exists(clazz.getName())) {
			// 存在，则直接执行原子加一操作
			return getAndIncrement(clazz.getName());
		} else {
			int i = 0;
			while (i < RETRY_LOCK_TIMES) {
				String requestId = UUID.randomUUID().toString();
				boolean success = tryDistributeLock(clazz.getName() + LOCK_SUFFIX, requestId, 3);//
				if (success) {
					try {
						// 双重检查机制
						if (exists(clazz.getName()))
							return getAndIncrement(clazz.getName());
						// 从数据库获取最大值
						String tableName = null, idFieldName = null;
						Table[] ts = clazz.getDeclaredAnnotationsByType(Table.class);
						if (ts != null && ts.length == 1) {
							Table table = ts[0];
							tableName = table.name();
						}
						if (StringUtils.isEmpty(tableName))
							throw new RuntimeException("未能解析到实体类对应的表名，请检查实体类是否包含@Table注解。");
						Field[] fields = clazz.getDeclaredFields();
						for (Field f : fields) {
							Id[] ids = f.getAnnotationsByType(Id.class);
							if (null != ids && ids.length == 1) {
								Column[] cs = f.getAnnotationsByType(Column.class);
								if (null != cs && cs.length == 1) {
									idFieldName = cs[0].name();
								} else {
									idFieldName = f.getName().toUpperCase();
								}
							}
						}
						if (StringUtils.isEmpty(idFieldName))
							throw new RuntimeException("未能解析到实体类对应的主键字段，请检查是否包含@Id， @Column注解。");
						DataSource ds = SpringBeanHelper.getBean(DataSource.class);
						JdbcTemplate jdbcTemplate = new JdbcTemplate(ds);
						List<Long> ids = jdbcTemplate.queryForList("select max(" + idFieldName + ") from " + tableName,
								Long.class);
						long max = 0;
						if (null != ids && ids.size() == 1 && null != ids.get(0)) {
							max = ids.get(0);
						}
						RedisTemplate<String, String> redisTemplate = (RedisTemplate<String, String>) SpringBeanHelper
								.getBean(REDIS_TEMPLATE_BEAN_NAME);
						redisTemplate.opsForValue().append(clazz.getName(), max + "");
						return getAndIncrement(clazz.getName());
					} catch (Exception e) {
						throw new RuntimeException(e);
					} finally {
						releaseDistributeLock(clazz.getName() + LOCK_SUFFIX, requestId);
					}
				} else {
					i++;
				}
			}
			throw new RuntimeException("尝试超过三次获取分布式锁失败，无法取得递增主键值");
		}
	}

	/**
	 * 尝试获取分布式锁
	 * 
	 * @param jedis
	 *            我们使用key来当锁，因为key是唯一的
	 * @param lockKey
	 * @param requestId
	 * @param expireTime
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static boolean tryDistributeLock(String lockKey, String requestId, int expireTime) {
		@SuppressWarnings("rawtypes")
		RedisTemplate redisTemplate = (RedisTemplate) SpringBeanHelper.getBean(REDIS_TEMPLATE_BEAN_NAME);
		return (boolean) redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
			Jedis jedis = (Jedis) redisConnection.getNativeConnection();
			String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
			if (LOCK_SUCCESS.equals(result)) {
				return Boolean.TRUE;
			}
			return Boolean.FALSE;
		});
	}

	/**
	 * 分布式锁解锁
	 * 
	 * @param jedis
	 * @param lockKey
	 *            分布式锁KEY值
	 * @param requestId
	 *            解锁客户端
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static boolean releaseDistributeLock(String lockKey, String requestId) {
		@SuppressWarnings("rawtypes")
		RedisTemplate redisTemplate = (RedisTemplate) SpringBeanHelper.getBean(REDIS_TEMPLATE_BEAN_NAME);
		return (boolean) redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
			Jedis jedis = (Jedis) redisConnection.getNativeConnection();
			Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
					Collections.singletonList(requestId));
			if (RELEASE_SUCCESS.equals(result)) {
				return Boolean.TRUE;
			}
			return Boolean.FALSE;
		});
	}

	/**
	 * 是否存在key
	 * 
	 * @param key
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private static boolean exists(final String key) {
		@SuppressWarnings("rawtypes")
		RedisTemplate redisTemplate = (RedisTemplate) SpringBeanHelper.getBean(REDIS_TEMPLATE_BEAN_NAME);

		return (boolean) redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
			Jedis jedis = (Jedis) redisConnection.getNativeConnection();
			if (jedis.exists(key)) {
				return true;
			}
			return false;
		});
	}

	/**
	 * 获取并自增
	 * 
	 * @param key
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private static Long getAndIncrement(final String key) {
		@SuppressWarnings("rawtypes")
		RedisTemplate redisTemplate = (RedisTemplate) SpringBeanHelper.getBean(REDIS_TEMPLATE_BEAN_NAME);

		return (long) redisTemplate.execute((RedisCallback<Long>) redisConnection -> {
			Jedis jedis = (Jedis) redisConnection.getNativeConnection();
			return jedis.incr(key);
		});

	}
}
